/**
 * Builds a widget that allows the user to pick a series of dates, and displays
 * an inferred recurrence rule.
 *
 * @param {HTMLElement} container the node to which the series date picker is
 *   attached.
 * @param {Date} visibleDate the date which is initially visible.
 * @param {Object} cfg extra configuration parameters including:
 *   onSelect - a function that receives the selected dates when a date is
 *     selected or unselected.
 *   onCancel - a function that is notified when the user clicks the cancel
 *     button.
 *   onConfirm - a function that receives the completed rule when the user
 *     clicks the confirm button.
 *   confirmButtonText - a string displayed in the confirm button.
 *   dayAbbrevs - abbreviations for weekdays.  Defaults to English defaults.
 *   wkst - the first day of the week in the week.
 */
function attachDateSeriesPicker(container, visibleDate, cfg) {
  var wkst = cfg.wkst || 0;
  visibleDate = new Date(+visibleDate);

  var onSelect = cfg.onSelect;
  var onCancel = cfg.onCancel;
  var onConfirm = cfg.onConfirm;
  var confirmButtonText = cfg.confirmButtonText || "OK";
  var dayAbbrevs = cfg.dayAbbrevs || ['Su', 'M', 'Tu', 'W', 'Th', 'F', 'Sa'];

  var visibleMonth = visibleDate.getUTCMonth();
  var visibleYear = visibleDate.getUTCFullYear();
  var datesSelected = [];
  var datesSelectedSet = {};

  var doc = container.ownerDocument;
  function makeEl(name, className) {
    var el = doc.createElement(name);
    el.className = className;
    if (name === 'button') { el.type = 'button'; }
    return el;
  }
  function addText(container, text) {
    container.appendChild(doc.createTextNode(text));
  }
  function replaceText(node, text) {
    while (node.firstChild) {
      node.removeChild(node.firstChild);
    }
    addText(node, text);
  }

  var node = makeEl('div', 'date-series-picker');

  // Create a year display with left and right buttons, and an editable text
  // field.
  var yearNode = makeEl('div', "year");
  var yearLeft = makeEl('button', "year-prev");
  addText(yearLeft, '\u00ab');
  var yearRight = makeEl('button', "year-next");
  addText(yearRight, '\u00bb');
  var yearDisplay = makeEl('input', "year-disp");
  function adjustYear(delta) {
    return function () {
      var newYear;
      if (delta) {
        newYear = visibleYear + delta;
      } else {
        newYear = (yearDisplay.value) | 0;
        if (!(newYear >= 1900)) {
          newYear = visibleYear;
        }
      }
      visibleYear = newYear;
      updateDisplay();
    };
  }
  yearNode.appendChild(yearLeft);
  yearNode.appendChild(yearDisplay);
  yearNode.appendChild(yearRight);
  node.appendChild(yearNode);
  yearLeft.onclick = adjustYear(-1);
  yearRight.onclick = adjustYear(1);
  yearDisplay.onblur = adjustYear();
  yearDisplay.onfocus = function () { this.select(); };

  // Create a month display with left and right buttons, and an editable text
  // field.
  var monthNode = makeEl('div', "month");
  var monthLeft = makeEl('button', "month-prev");
  addText(monthLeft, '\u00ab');
  var monthRight = makeEl('button', "month-next");
  addText(monthRight, '\u00bb');
  var monthDisplay = makeEl('input', "month-disp");
  function adjustMonth(delta) {
    return function () {
      var newMonth;
      if (delta) {
        newMonth = visibleMonth + delta;
      } else {
        newMonth = (monthDisplay.value) | 0;
        if (newMonth >= 1 && newMonth <= 12) {
          newMonth = newMonth - 1;
        } else {
          newMonth = visibleMonth;
        }
      }
      var newYear = visibleYear;
      if (newMonth < 0) {
        --newYear;
        newMonth += 12;
      } else if (newMonth >= 12) {
        ++newYear;
        newMonth -= 12;
      }
      visibleMonth = newMonth;
      visibleYear = newYear;
      updateDisplay();
    };
  }
  monthNode.appendChild(monthLeft);
  monthNode.appendChild(monthDisplay);
  monthNode.appendChild(monthRight);
  node.appendChild(monthNode);
  monthLeft.onclick = adjustMonth(-1);
  monthRight.onclick = adjustMonth(1);
  monthDisplay.onblur = adjustMonth();
  monthDisplay.onfocus = function () { this.select(); };

  var calendar = makeEl('table', 'cal');
  var weekDays = makeEl('thead', 'hdrs');
  calendar.appendChild(weekDays);
  var weekDayRow = makeEl('tr', 'week-days');
  weekDays.appendChild(weekDayRow);
  for (var i = 0; i < 7; ++i) {
    var weekDay = makeEl('th', '');
    addText(weekDay, dayAbbrevs[i]);
    weekDayRow.appendChild(weekDay);
  }
  var dates = makeEl('tbody', 'weeks');
  calendar.appendChild(dates);
  for (var j = 0; j < 6; ++j) {
    var weekRow = makeEl('tr', '');
    dates.appendChild(weekRow);
    for (var i = 0; i < 7; ++i) {
      var day = makeEl('td', '');
      addText(day, '');
      day.onclick = daySelected(j, i);
      day.tabIndex = 0;
      weekRow.appendChild(day);
    }
  }
  function daySelected(j, i) {
    var cellIndex = j * 7 + i;
    return function () {
      var d = date(visibleYear, visibleMonth, cellIndex - day1Cell() + 1);
      if (datesSelectedSet[+d] === datesSelectedSet) {
        delete datesSelectedSet[+d];
        for (var i = 0; i < datesSelected.length; ++i) {
          if (+datesSelected[i] === +d) {
            datesSelected.splice(i, 1);
            break;
          }
        }
      } else {
        datesSelectedSet[+d] = datesSelectedSet;
        datesSelected.push(d);
      }
      updateDisplay();
      var currentRule = inferDateSeries(datesSelected.slice(0), wkst);
      replaceText(
        description,
        currentRule.confidence >= .75 && currentRule.dates.length > 1
        ? dateSeries(currentRule.dates[0], currentRule.rule).toString()
        : currentRule.dates.length
          + " date" + (currentRule.dates.length !== 1 ? "s" : ""));
      if (onSelect) {
        onSelect(datesSelected.map(function (x) { return new Date(+x); }),
               new Date(+d));
      }
    };
  }
  node.appendChild(calendar);

  var controls = makeEl('div', 'ctrls');
  if (onConfirm) {
    var confirmButton = makeEl('button', 'conf');
    addText(confirmButton, confirmButtonText);
    controls.appendChild(confirmButton);
    confirmButton.onclick = function () {
      onConfirm(inferDateSeries(datesSelected.slice(0), wkst));
    };
  }
  if (onCancel) {
    var cancelButton = makeEl('button', 'cancel');
    addText(cancelButton, "Cancel");
    controls.appendChild(cancelButton);
    cancelButton.onclick = function () { onCancel(); };
  }
  node.appendChild(controls);

  var description = makeEl('div', 'desc');
  node.appendChild(description);

  // The index of the cell containing the first day of the visible month.
  function day1Cell() {
    var dow = date(visibleYear, visibleMonth, 1).getUTCDay();
    return (dow - wkst + 7) % 7;
  }

  function updateDisplay() {
    yearDisplay.value = "" + visibleYear;
    monthDisplay.value = (visibleMonth < 9 ? "0" : "") + (visibleMonth + 1);

    var nDays = countDaysInMonth(visibleYear, visibleMonth);
    var cells = dates.getElementsByTagName('td');
    var leastCellInMonth = day1Cell();
    var greatestCellInMonth = leastCellInMonth + nDays - 1;
    for (var i = 0, n = cells.length; i < n; ++i) {
      var cell = cells[i];
      var cellText = "", cellClass = "";
      var cellDay = (i - leastCellInMonth + 1);
      var cellDate = date(visibleYear, visibleMonth, cellDay);
      var isSelectedCell = datesSelectedSet === datesSelectedSet[+cellDate];
      if (1 <= cellDay && cellDay <= nDays) {
        cellText = "" + cellDay;
        cellClass = isSelectedCell ? "sel in-month" : "in-month";
      } else {
        cellClass = isSelectedCell ? "sel not-in-month" : "not-in-month";
      }
      if (+cellDate === +visibleDate) {
        cellClass += " anchor";
      }
      cell.firstChild.nodeValue = cellText;
      cell.className = cellClass;
    }

    cells[28].parentNode.className = greatestCellInMonth < 28
      ? 'not-in-month' : '';
    cells[35].parentNode.className = greatestCellInMonth < 35
      ? 'not-in-month' : '';
  }

  container.appendChild(node);
  updateDisplay();
}
